Julia数据科学系列-DataFrames包
- DataFrames.jl
- 快速开始
- 基本操作
- 取子集
select/select!
和transform/transform!
: 操作列transform
和transform!
- 数据统计:
describe
和combine
- 数据替换
- 输入输出
Join
操作Split-Apply-Combine
数据操作策略Reshaping
和Pivoting
: 长宽表转换- 排序
- 分组数据
- 缺失值: Missing
- 扩展包: DataFramesMeta.jl
@transform[!]
@subset[!]
@combine
@orderby
@with
@eachrow[!]
@byrow
和@r...
: 逐行数据转换- 用
@passmissing
广播missing @astable
和AsTable
同时创建多列- 用
$
转义 - 用
^
忽略把对应的Symbol
解析成列名 - 用
@chain
宏管道操作
DataFrames.jl
快速开始
构建
DataFrame
类型
基本操作
取子集
subset
和subset!
: 操作行
subset(df::AbstractDataFrame, args...; skipmissing::Bool=false, view::Bool=false)
subset(gdf::GroupedDataFrame, args...; skipmissing::Bool=false, view::Bool=false,
ungroup::Bool=true)
select/select!
和transform/transform!
: 操作列
复杂的按列过滤函数
永远返回DataFrame
默认会从原始df中拷贝一份选择的列, 可以用
copycols=false
参数关闭copy
transform
和transform!
和select
的用法基本一样, 唯一区别是transform/transform!
会同时返回原始df中的所有列。
一个复杂的例子, 按行计算sum
, num of elements
, mean
, 同时忽略missing
.
数据统计: describe
和combine
describe
: 返回一个df, 记录表格每列基本统计信息mean, min, median, max, nmissing, eltype
combine
: 应用统计函数
数据替换
replace!
: 对单列进行替换
以上操作等价于df.a = replace(df.a, "None" => "c")
, 但是是in-place
的操作, 不需要重新分配内存。
如果需要对多列进行in-place
的替换操作, 则可以用广播
来实现:
最后一句如果把.=
换成=
, 则不会执行in-place
操作, 会额外分配内存:
输入输出
用CSV.jl读写各种分隔文本格式:
CSV.File
,CSV.read
,CSV.write
using CSV
df = DataFrame(CSV.File("input.csv")) # 读
CSV.write("output.csv", df) # 写
Julia标准库中的
DelimitedFiles
也可以用来读写DataFrame:
using DelimitedFiles, DataFrames
data, header = readdlm("in.csv", ',', header=true);
df_raw = DataFrame(data, vec(header))
df = identity.(df_raw)
#写:
writedlm("test.csv", Iterators.flatten(([names(df)], eachrow(df))), ',')
header 是一个一维矩阵Matrix, 需要转成Vector;
df_raw中每列的类型都是
Any
, 用identity缩小每列的类型, 这是因为data
是一个Matrix
, 内部元素应该是同一种类型(在这个例子中是Any
), 所以转成的DF类型是Any
;以上这些操作, 在
CSV.jl
中都是自动执行的, 所以还是用CSV.jl
方便啊;
DataFrame
的广播操作是按列的咯?
用
JSONTables.jl
读写JSON用
XLSX.jl
读写XLSX其他格式平时不常用, 就不列举了, 用时自搜
Join
操作
功能与SQL Join类似
innerjoin
leftjoin
rightjoin
outerjoin
semijoin
: 类似innerjoin
, 但是输出是以第一个df为参照antijoin
: 输出第一个df有,第二个df中没有的crossjoin
: 输出是所有df的列的笛卡尔积
innerjoin(df1, df2, on = :ID) # 大部分join都用同一种语法
innerjoin(df1, df2, on = :ID_df1 => :ID_df2) # 如果要合并的列名字不一样, 用`=>`指示对应关系
crossjoin(df1, df2, makeunique = true) # crossjoin不使用`on`关键词
如果参考列有重复值,
inner|outer|left|right
会把所有排列组合都输出;可以用
validate=(true,true)
来指示对哪些参考列对进行检查, 如果这些列对有重复值, 会报错;
source
关键词可以用来指示参考列在两个输入df中是不是共有的:
julia> outerjoin(df1, df2, on=:ID, validate=(true, true), source=:source)
3×4 DataFrame
Row │ ID Name Job source
│ Int64 String? String? String
─────┼─────────────────────────────────────
1 │ 20 John Lawyer both
2 │ 40 Jane missing left_only
3 │ 60 missing Doctor right_only
Split-Apply-Combine
数据操作策略
把数据集拆分成不同的分组;
对某些分组应用特定的方法;
合并结果成新的数据集;
DataFrame中实现的方式是利用groupby
创建GroupedDataFrame
数据类型, 然后结合combine
, select
, transform
等操作对齐进行数据处理。
groupby
: 对DataFrame进行分组combine
: 不限制返回行数, 行的顺序取决于group的顺序, 很适合分组计算统计信息select
: 返回跟原始df一样的行数和顺序的新列transform
: 返回原始df以及追加的新的列
支持的处理函数可以是:
标准的列指示信息:
Integers
,Symbols
,Vector{Integers|Symbols}
,Strings
,All
,Cols
,:
,Between
,Not
, Regexcols => function
键值对: 这种方法调用时, 会自动生成结果列的名字:默认是拼接输入列的名字和函数的名字,function
需要返回一个值或者一个向量cols => function => target_cols
方式: 显式地声明输出列的名字, 可以是单个值, 向量, 或者是AsTable
, 也可以是一个以cols
中的名字为参数, 返回目标列名字的函数cols => target_cols
方式: 重命名nrow
或nrow => target_cols
: 快速统计行数, 不显式声明名字的话, 输出名字默认是:nrow
2和5中键值对组成的向量或矩阵
SubDataFrame
类型支持的函数: 不太推荐, 因为表现力不行
当出现
x => y
这种用法时, 会先检查是不是nrow => target_cols
, 如果不是, 则一律按照cols => function
的逻辑去处理如果
cols
或者target_cols
中是All
,Cols
,Between
或者Not
这几个特定方法时, 可以用.=>
进行广播, 等同于广播names(df, cols)
或者names(df, cols)
这解答了我之前的疑惑: 就是按列广播, 实际上是按列名广播cols => function [=> target_cols]
这种用法中, 如果cols
是AsTable
对象, 则会把一个由cols
当名字的具名元组传递给function
如果
cols
和target_cols
都没有指定, 只传递了function
, 则应该返回DataFrame, Matrix, NamedTuple, DataFrameRow
中的一种, 返回其他类型都只会存成一列如果
target_cols
是Symbol
或String
,function
应该只返回一列, 这时返回DataFrame, Matrix, NamedTuple, DataFrameRow
的函数会报错如果
target_cols
是Vector{Symbol}
或Vector{String}
或AsTable
,function
应该返回多列, 如果function
返回值是AbstractVector
, 则其每个元素都得支持keys
方法, 而且keys
方法必须得返回Symbol, String, Integer
, 如果返回Integer
, 则输出列名默认是x
开头(x1, x2 ...
)如果
function
的返回值是其他类型, 会被认为是Table.jl
支持的表格类型, 然后尝试利用Tables.columntable
方法获取其名字function
的返回值中, 如果是Ref
或者零维向量(就是空向量?)会被当作是单独的一行
当julia多核启动时, 对DataFrame调用转换函数, 会自动对每个转换事件进行并行(除非一些专门优化过的计算, 比如sum
, 会把所有分组的计算单线程串行), 因此调用的函数应该是干净的
(即不能修改全局变量), 或者用进程锁(太高端了暂时用不上)
可以用ByRow
结构表明函数是按行执行, 而不是按列执行。
keepkeys
: 分组的列是否要在结果DF中保留ungroup
: 输出DataFrame
还是GroupedDataFrame
copycols
: 原始DF中没被操作的列, 是否要copyrenamecols
:cols => functions
形式中,自动生成的结果列名字是否要加上原始列的名字
例子:
using DataFras, CSV, Statistics
fpath = joinpath(dirname(pathof(DataFrames)), "..", "docs", "src", "assets", "iris.csv");
iris = CSV.read(fpath, DataFrame)
gdf = groupby(iris, :Species)
combine(gdf, :PetalLength => mean)
combine(gdf, nrow)
combine(gdf, nrow, :PetalLength => mean => :mean)
combine(gdf, [:PetalLength, :SepalLength] => ((p, s) -> (a=mean(p)/mean(s), b=sum(p))),
AsTable)
combine(gdf, AsTable([:PetalLength, :SepalLength]) =>
x -> std(x.PetalLength)/std(x.SepalLength))
combine(x -> std(x.PetalLength) / std(x.SepalLength), gdf)
combine(gdf, 1:2 => cor, nrow)
combine(gdf, :PetalLength => (x -> [extrema(x)]) => [:min, :max])
select(gdf, 1:2 => cor)
transform(gdf, :Species => x -> chop.(x, head=5, tail=0))
# do block is supported, but sould be avoided because it is slow:
combine(gdf) do df
(m = mean(df.PetalLength), s² = var(df.PetalLength))
end
for subdf in groupby(iris, :Species)
println(size(subdf, 1))
end
for (key, subdf) in pairs(groupby(iris, :Species))
println("Number of data points for $(key.Species): $(nrow(subdf))")
end
groupby结果的key是
DataFrames.GroupKey
类型, 可以当作是一种NamedTuple
groupby可以当作对DataFrame添加了查找索引, 可以通过
Tuple
或者NamedTuple
来快速跳到指定索引:
df = DataFrame(g=repeat(1:1000, inner=5), x=1:5000);
gdf = groupby(df, :g)
gdf[(g=500,)] # 用一个元素的NamedTuple, 获取分组是500的行
gdf[[(500,), (501,)]] # 用Vector{NamedTuple}, 获取两个分组
用
valuecols
获取所有没分组的列:
combine(gdf, valuecols(gdf) .=> mean)
GroupedDataFrame
不是copy, 而是view, 所以其父DF的对应列不能改变, 也不能改变行数, 否则调用子gdf时会报错如果想父DF的改动不影响子GDF, 则要用父df的view创建gdf:
gdf = groupby(view(df, :, :), :id)
SubDataFrames
类型可以让我们快速获得df的子集, 实现类似SQL的where
功能:
df = DataFrame(a=1:5)
sdf = @view df[2:3, :] # SubDataFrame type
transform(sdf, :a => ByRow(string)) # 创建新的DataFrame
transform!(sdf, :a => ByRow(string)) # 本地更改sdf, 类型还是SubDataFrame
df # 更改SubDataFrame, 会对父df也一样更改, 这里df中没操作的行, 对应列填充missing
select!(sdf, :a => -, renamecols=false) # 再操作, 这里原地操作
df
Reshaping
和Pivoting
: 长宽表转换
stack
: 宽表变长表, 会自动进行类型提升
stack(iris, 1:4) # 把1-4列当成variable
stack(iris, [:SepalLength, :SepalWidth, :PetalLength, :PetalWidth])
stack(iris, Not(:Species))
# stack的第三个参数指定需要重复的列(指示列)
stack(iris, [:SepalLength, :SepalWidth], :Species)
unstack
: 长表转宽表
unstack(df::AbstractDataFrame, rowkeys, colkey, value; renamecols::Function=identity,
allowmissing::Bool=false, allowduplicates::Bool=false, fill=missing)
unstack(df::AbstractDataFrame, colkey, value; renamecols::Function=identity,
allowmissing::Bool=false, allowduplicates::Bool=false, fill=missing)
unstack(df::AbstractDataFrame; renamecols::Function=identity,
allowmissing::Bool=false, allowduplicates::Bool=false, fill=missing)
iris.id = 1:size(iris, 1)
longdf = stack(iris, Not([:Species, :id]))
unstack(longdf, :id, :variable, :value)
# 如果剩下的col是unique的, 可以不提供id variable:
unstack(longdf, :variable, :value)
# 甚至可以不提供variable和value:
unstack(longdf)
# 添加view=true, 不copy新数据, 而是创建原数据的view, 会节省内存
stack(iris, view=true)
view=true
时, 会创建几个向量:EachRepeatedVector
对应:variable
;StackedVector
对应:value
;Repeatedvector
对应ID cols
permnutedims
: 反转dfpermutedims(df, 1)
排序
直接调用sort/sort!
, 可配合ref
,by
关键词和order
方法使用
sort!(iris) # 每列都逐级参与排序
sort!(iris, rev = true)
sort!(iris, [:Species, :SepalWidth]) # 指定排序的列
sort!(iris, [order(:Species, by=length), order(:SepalLength, rev=true)]) # 指定排序方式
sort!(iris, [:Species, :PetalLength], rev=[true, false]) # 另一种指定排序方式的语法
分组数据
groupby
操作, 目前有两种类型帮助实现:
来自PooledArrays.jl包中的
PooledVector
, 只是用来减少存储来自CategoricalArrays.jl包中的
CategoricalVector
, 还提供函数取回分组顺序, 在分析和画图中很有用 这个更活跃一点, 应用范围也更广
缺失值: Missing
Missing
类型的唯一实例是missing
skipmissing(x)
方法可以过滤掉x中missing的值, 返回的是一个迭代器coalesce
可以用来替换missing为其他值:coalesce.(x, 0)
, 该方法是针对指定值的, 所以对向量要广播dropmissing/dropmissing!
: 去掉df中有missing的行,dropmissing(df, :x)
指定行, 设置disallowmissing=true
参数让输出的df不支持missingallowmissing[!]
和disallowmissing[!]
: 把指定df(的指定列)改成支持/不支持missing
Missings.jl包提供了一堆专门处理缺失值的函数, 其中一个
passmissing(func)
可以跳过用missing执行func
, 还有Missing.replace
,nonmissingtype
,missings(N)
等, 这里不展开了
扩展包: DataFramesMeta.jl
官方文档 HERE
DataFrames.jl
中的select
, transform
和combine
等方法很强, 但是逻辑上有时候略显繁琐, 受R中dplyr
和C#中LINQ
的启发, DataFramesMeta.jl
提供了这些方法的镜像宏, 可以用更简洁的语法来操作。除此之外, DataFramesMeta.jl
还提供了其他宏操作, 如@orderby, @subset[!], @r[transform,select,orderby,subset], @by, @with, @eachrow, @byrow, @passmissing, @astable, @chain
等。
DataFrames.jl
中提供了Between, All, Cols, Not
等列操作, 这些在DataFramesMeta.jl
中暂不支持。@select[!]
@select
返回新的DF, 每列都是重新分配内存的@select!
直接操作原DF相比
select
, 采用了更简洁的语法::y = f(:x)
df = DataFrame(x = [1, 1, 2, 2], y = [1, 2, 101, 102]);
gdf = groupby(df, :x);
@select(df, :x, :y)
@select(df, :x2 = 2 * :x, :y)
@select(gdf, :x2 = 2 .* :y .* first(:y))
@select!(df, :x, :y)
@select!(df, :x = 2 * :x, :y)
@select!(gdf, :y = 2 .* :y .* first(:y))
@transform[!]
逻辑与@select
类似:
@transform(df, :x2 = 2 * :x, :y)
@transform(gdf, :x2 = 2 .* :y .* first(:y))
@subset[!]
using Statistics
outside_var = 1;
@subset(df, :x .> 1)
@subset(df, :x .> outside_var)
@subset(df, :x .> outside_var, :y .< 102) # 两个条件是 and 的关系
@subset(gdf, :x .> mean(:x))
@combine
@combine(gdf, :x2 = sum(:y))
@combine(gd, :x2 = :y .- sum(:y))
@combine(gd, $AsTable = (n1 = sum(:y), n2 = first(:y)))
注意最后一个例子中用$
转义表明输出是一个Table
格式, $
转义的具体用法将在下文说明。
@combine
的第一个参数需要是DF或GDF, 而combine
可以把函数作为第一个参数, 而把GDF当作第二个参数:
# 不支持的:
@combine((a=sum(:x), b=sum(:y)), gdf)
# 支持:
@combine(gdf, $AsTable = (a = sum(:x), b = sum(:y)))
@orderby
@orderby
对DF的多列进行排序, 只支持DataFrame
, 不支持GroupedDataFrame
。
@orderby(df, -1 .* :x)
@orderby(df, :x, :y .- mean(:y))
@with
@with df
后边跟着的代码块中出现的所有Symbol
类型都会被解析成是df对应列的数组, 这样需要对一个df的很多列做一系列下游计算的时候, 就可以不用重复写df.colname
了, 很方便:
df = DataFrame(x = 1:3, y = [2, 1, 2])
x = [2, 1, 0]
@with(df, :y .+ 1)
@with(df, :x + x)
x = @with df begin
res = 0.0
for i in 1:length(:x)
res += :x[i] * :y[i]
end
res
end
# 用^()包裹的Symbols不会被展开成数组
@with(df, df[:x .> 1, ^(:y)])
# 上边这个结果等价于:
df[[1,2,3] .> 1, :y]
# :x 被展开了, :y 没有
@with
会生成一个函数, 所以@with
内部定义的变量是局部变量. 在@with
代码块内部给其外部变量赋值时, 需不需要添加global
关键字取决于其外部代码块的属性:
如果外部代码块是全局的, 则
@with
内部需要添加global
关键字才能使用该变量;如果外部代码块是局部的(函数内或者
let
语句块内), 则不需要添加global
;
@with
会生成函数, 所以在使用return
的时候要小心: function data_transform(df; returnearly = true)
if returnearly
@with df begin
z = :x + :y
return z
end
else
return [1, 2, 3]
end
return [4, 5, 6]
end
[4, 5, 6]
, 因为@with
内部的return
是属于@with
生成的匿名函数的, 不是data_transform
的。接下来将要介绍的@eachrow
是基于@with
实现的, 所以也同样需要注意这个问题。
@eachrow[!]
逐行操作DF并返回操作后的DF的每行, 支持控制流和begin end
代码块。
df = DataFrame(A = 1:3, B = [2, 1, 2])
df2 = @eachrow df begin
:A = :B + 1
end
类似@with
, 由于@eachrow
生成一个函数代码块, 所以要引用外部变量时, 需要用let
代码块包裹, 或者用global
关键字(推荐用let
, 更易懂):
df = DataFrame(A = 1:3, B = [2, 1, 2], C = [-4,2,1])
# 用let包裹, 让x成为局部变量
let x = 0.0
@eachrow df begin
if :A < :B
x += :A * :C
end
end
x
end # x = -4.0
# y是全局变量, 在@eachrow内要用global关键字
y = 0.0
@eachrow df begin
if :A < :B
global y += :A * :C
end
end;
y # y = -4.0
@echorow
中, 可以用@newcol
宏(语法:@newcol :x::Vector{T}
)来分配新的类型为T
的列:
df = DataFrame(A = 1:3, B = [2, 1, 2])
df2 = @eachrow df begin
@newcol :colX::Vector{Float64}
@newcol :colY::Vector{Union{Int, Missing}}
@newcol :colZ::Vector{String}
:colX = :B == 2 ? pi * :A : :B
if :A > 1
:colY = :A * :B
else
:colY = missing
end
:colZ = string(:A)
end
@byrow
和@r...
: 逐行数据转换
@byrow
可以方便地对df逐行操作, DataFramesMeta.jl
中一系列的行操作宏都是基于@byrow
的:
@rtransform
,@rtransform!
@rselect
,@rselect!
@rorderby
@rsubset
,@rsubset!
在DataFram.jl
中, 有ByRow
函数, 可以当作是按行广播操作: ByRow(f)(x, y)
≈ f.(x, y)
。@byrow
可以理解成在DataFramesMeta.jl
的宏中使用的ByRow
。
@byrow
不是真正的宏, 不能用于DataFramesMeta.jl
的宏之外。
#以下两行代码是等价的:
@transform(df, @byrow :y = :x == 1 ? true : false)
transform(df, :x => ByRow(x -> x == 1 ? true : false) => :y)
为了避免多次操作时重复写@byrow
, 可以把@byrow
写在代码块的开头, 则代码块中的所有操作都是逐行的了:
@subset df @byrow begin
:a > 1
:b < 5
end
@byrow
也可以用于GroupedDataFrame
, 但是与ByRow
类似, 在用给GDF中, 分组信息是不会被考虑的:
# 以下代码是等价的:
@transform(df, @byrow :y = f(:x))
@transform(groupby(df, :g), @byrow :y = f(:x))
用@passmissing
广播missing
很多Julia中的函数是不支持missing的广播的(如parse(Int, missing)
会报错)。 Missing.jl
包中提供了passmissing
函数来处理missing。相应地, DataFramesMeta.jl
中提供了@passmissing
宏来在其他宏操作中支持missing。
#以下代码是等价的:
@transform df @byrow @passmissing :c = f(:a, :b)
transform(df, [:a, :b] => ByRow(passmissing(f)) => :c)
更具体的例子:
julia> no_missing(x::Int, y::Int) = x + y;
julia> df = DataFrame(a = [1, 2, missing], b = [4, 5, 6])
3×2 DataFrame
Row │ a b
│ Int64? Int64
─────┼────────────────
1 │ 1 4
2 │ 2 5
3 │ missing 6
julia> @transform df @passmissing @byrow :c = no_missing(:a, :b)
3×3 DataFrame
Row │ a b c
│ Int64? Int64 Int64?
─────┼─────────────────────────
1 │ 1 4 5
2 │ 2 5 7
3 │ missing 6 missing
julia> df = DataFrame(x_str = ["1", "2", missing])
3×1 DataFrame
Row │ x_str
│ String?
─────┼─────────
1 │ 1
2 │ 2
3 │ missing
julia> @rtransform df @passmissing :x = parse(Int, :x_str)
3×2 DataFrame
Row │ x_str x
│ String? Int64?
─────┼──────────────────
1 │ 1 1
2 │ 2 2
3 │ missing missing
@astable
和AsTable
同时创建多列
@astable
@astable
:
julia> df = DataFrame(a = [1, 2, 3], b = [400, 500, 600]);
julia> @transform df @astable begin
ex = extrema(:b)
:b_first = :b .- first(ex)
:b_last = :b .- last(ex)
end
3×4 DataFrame
Row │ a b b_first b_last
│ Int64 Int64 Int64 Int64
─────┼───────────────────────────────
1 │ 1 400 0 -200
2 │ 2 500 100 -100
3 │ 3 600 200 0
AsTable
在表达式右侧的操作
AsTable(cols)
同时对多列操作, 当把AsTable用在表达式右边的时候:
如果使用了
AsTable(cols)
, 就不要在代码块中再引用其他列了AsTable
支持配合Not, Between, r""
等使用AsTable
内部的东西都是被强制转义的, 所以不需要在其内部使用用$
df = DataFrame(a = [11, 14], b = [17, 10], c = [12, 5]);
vars = ["a", "b"];
@rtransform df :y = sum(AsTable(vars))
@rtransform df :y = sum(AsTable([:a, :b]))
# AsTable还支持用变量的名字进行操作
function fun_with_new_name(x::NamedTuple)
nms = string.(propertynames(x))
new_name = Symbol(join(nms, "_"), "_sum")
s = sum(x)
(; new_name => s) # (; ) => 定义具名元组
end
@rtransform df $AsTable = fun_with_new_name(AsTable([:a, :b]))
@rsubset df sum(AsTable(vars)) > 25
:y = first(AsTable("a")) # AsTable内部强制转义
DataFrames.jl
中可以当作source
, 进行source => fun => dest
这种操作。在
DataFramesMeta.jl
中, :y = f(AsTable(cols))
会被翻译成AsTable(cols) => f => :y
, 所以, 在用:y = fun
的操作时, 不能混用AsTable
和:col_id
::y = sum(AsTable(cols)) + :d
会报错。
AsTable
和@astable
出现的三种场合:
表达式左边:
$AsTable = f(:a, :b)
在表达式内用
@astable
表达式右边:
AsTable(cols)
这三种用法的区别总结如下:
操作 | 目的 | 注意 |
---|---|---|
左侧的$AsTable | 批量创建多列, 这些列的名字是根据脚本生成的(不提前知道) | 需要$ 转移 |
@astable | 批量创建多列, 这些列名字提前已知 | |
右侧的AsTable | 同时处理多列 | 需要输入列名 |
用$
转义
在DataFrameMeta.jl
中, 用$
充当DF的列名变量的转义符, 变量存储的值可以是Symbol
或String
或Int
表示列号(有限制), 也可以直接对字面量进行转义。
df = DataFrame(A = 1:3, B = [2, 1, 2])
nameA = :A
nameA_string = "A"
df2 = @transform(df, :C = :B - $nameA)
df2 = @transform(df, :C = :B - $nameA_string)
df2 = @transform(df, :C = :B - $"A")
df2 = @transform(df, :C = :B - $:A)
$
也可以用于创建新列:
df = DataFrame(A = 1:3, B = [2, 1, 2])
newcol = "C"
@select(df, $newcol = :A + :B)
@by(df, :B, $("A complicated" * " new name") = first(:A))
nameC = "C"
df3 = @eachrow df begin
@newcol $nameC::Vector{Int}
$nameC = :A
end
当用$
转义Int
时, 有限制: 不允许混合使用Int
的转义和其他类型的列名表示:
@transform(df, :y = $1 + $2) # 正常
@transform(df, :y = :A + $2) # 报错
# 本质上是因为DataFrame在`source => fun => dest`表达式中, 要求`source`必须是同一种类型:
transform(df, [:A, :B] => (+) => :y) # 正常
transform(df, [:A, "B"] => (+) => :y) # 报错
@with
和@eachrow
也有这种限制: 整数引用$1, $2
不能与Symbol
或者String
类型的列引用共同使用。
被$()
包裹的函数会绕过DataFramesMeta.jl的匿名函数, 直接传递给DataFrames.jl
的函数。这个特性使得src => func => dest
可以通过$()
包裹, 用在DataFramesMeta.jl
的宏操作中:
using Statistics
df = DataFrame(a = [1, 2], b = [30, 40]);
@transform df $([:a, :b] .=> [sum mean])
# 也可以通过变量传递:
my_transformation = :a => (t -> t .+ 100) => :c;
@transform df begin
$my_transformation
:d = :b .+ 200
end
利用$
可以在宏操作中方便地选择多列:
select(df, [:a, :b])
@select df $[:a, :b]
select(df, r"^a")
@select df $(r"^a")
用
$()
进行多参数选择的时候, 必须保证所有的参数都被$()
包裹:@select df :y = f($[:a, :b])
会报错。DataFrame.jl
中不支持多列选择的函数, 其对应的宏也不支持:
subset(df, [:a, :b]) # error
@subset df $[:a, :b] # error
支持多列选择的宏有:
@select
@transform
@combine
@by
@orderby
和@with
没有对应的DataFrames.jl
中的函数, 所以尽量不要在这两个宏中用$
转义, 以后很有可能会有变动。
所有不被
$()
转义的参数, 都会被宏用于构建匿名函数, 且在这些表达式中, 只支持单列选择;被
$()
包裹的参数会被直接传给DataFrames.jl
中的对应函数, 所以可以允许多列选择, 包括:$[:x, :y]
,$["x", "y"]
,$[1, 2]
正则表达式
$(r"^a")
过滤方法:
$(Not(:x))
,$(Between(:a, :z))
@with
,@subset
,@orderby
不支持多列选择;可以用
$()
包裹src => fun => dest
进行操作, 但是不建议这么操作
用^
忽略把对应的Symbol
解析成列名
这个规则对所有DataFramesMeta
中的宏都适用。
df =DataFrame(x = [1, 1, 2, 2], y = [1, 2, 101, 102]);
@select(df, :x2 = :x, :x3 = ^(:x))
用@chain
宏管道操作
@chain
宏是来自Chain.jl
包的, DataFramesMeta
包重载了这个宏, 让其可以支持管道DF的宏操作:
using Statistics
df = DataFrame(a = repeat(1:5, outer = 20),
b = repeat(["a", "b", "c", "d"], inner = 25),
x = repeat(1:20, inner = 5))
x_thread = @chain df begin
@transform(:y = 10 * :x)
@subset(:a .> 2)
@by(:b, :meanX = mean(:x), :meanY = mean(:y))
@orderby(:meanX)
@select(:meanX, :meanY, :var = :b)
end
# Get the sum of all columns after
# a few transformations
@chain df begin
@transform(:y = 10 .* :x)
@subset(:a .> 2)
@select(:a, :y, :x)
reduce(+, eachcol(_))
end
# @aside 宏用以临时跳出管道, 也是Chain.jl中的
@chain df begin
@transform :y = 10 .* :x
@aside y_mean = mean(_.y)
@select :y_standardize = :y .- y_mean
end